/*
 * Copyright 2002-2007 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.baidu.bjf.remoting.protobuf;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.lang.StringUtils;

import com.baidu.bjf.remoting.protobuf.utils.FieldUtils;
import com.baidu.bjf.remoting.protobuf.utils.MethodUtils;

/**
 * IDL parsed proxy object
 * 
 * @author xiemalin
 * @since 1.0.2
 */
public class IDLProxyObject {

	private Codec codec;

	private Object target;

	private Class<?> cls;

	private final Map<String, ReflectInfo> cachedFields = new HashMap<String, ReflectInfo>();

	private boolean cached = true;

	/**
	 * get the cached
	 * 
	 * @return the cached
	 */
	public boolean isCached() {
		return cached;
	}

	/**
	 * set cached value to cached
	 * 
	 * @param cached
	 *            the cached to set
	 */
	public void setCached(boolean cached) {
		this.cached = cached;
	}

	/**
	 * default construtor to set {@link Codec} target
	 */
	public IDLProxyObject(Codec codec, Object target, Class<?> cls) {
		super();
		if (codec == null) {
			throw new IllegalArgumentException("param 'codec' is null.");
		}
		if (target == null) {
			throw new IllegalArgumentException("param 'target' is null.");
		}
		if (cls == null) {
			throw new IllegalArgumentException("param 'cls' is null.");
		}
		this.codec = codec;
		this.target = target;
		this.cls = cls;

	}

	public IDLProxyObject newInstnace() {
		try {
			Object object = cls.newInstance();
			return new IDLProxyObject(codec, object, cls);
		} catch (Exception e) {
			throw new RuntimeException(e.getMessage(), e);
		}
	}

	private IDLProxyObject doSetFieldValue(String fullField, String field,
			Object value, Object object, boolean useCache,
			Map<String, ReflectInfo> cachedFields) {
		Field f;
		// check cache
		if (useCache) {
			ReflectInfo info = cachedFields.get(fullField);
			if (info != null) {
				setField(value, info.target, info.field);
				return this;
			}
		}

		int index = field.indexOf('.');
		if (index != -1) {
			String parent = field.substring(0, index);
			String sub = field.substring(index + 1);

			try {
				f = FieldUtils.findField(object.getClass(), parent);
				if (f == null) {
					throw new RuntimeException("No field '" + parent
							+ "' found at class " + object.getClass().getName());
				}
				Class<?> type = f.getType();
				f.setAccessible(true);
				Object o = f.get(object);
				if (o == null) {
					boolean memberClass = type.isMemberClass();
					if (memberClass && Modifier.isStatic(type.getModifiers())) {
						Constructor<?> constructor = type
								.getConstructor(new Class[0]);
						constructor.setAccessible(true);
						o = constructor.newInstance(new Object[0]);
					} else if (memberClass) {
						Constructor<?> constructor = type
								.getConstructor(new Class[] { object.getClass() });
						constructor.setAccessible(true);
						o = constructor.newInstance(new Object[] { object });
					} else {
						o = type.newInstance();
					}
					f.set(object, o);
				}
				return put(fullField, sub, value, o);
			} catch (Exception e) {
				throw new RuntimeException(e.getMessage(), e);
			}
		}

		f = FieldUtils.findField(object.getClass(), field);
		if (f == null) {
			throw new RuntimeException("No field '" + field
					+ "' found at class " + object.getClass().getName());
		}
		if (useCache && !cachedFields.containsKey(fullField)) {
			cachedFields.put(fullField, new ReflectInfo(f, object));
		}
		setField(value, object, f);

		return this;
	}

	private IDLProxyObject put(String fullField, String field, Object value,
			Object object) {
		return doSetFieldValue(fullField, field, value, object, this.cached,
				this.cachedFields);
	}

	public IDLProxyObject put(String field, Object value) {
		return put(field, field, value, target);
	}

	/**
	 * @param value
	 * @param object
	 * @param f
	 * @throws SecurityException
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 */
	private void setField(Object value, Object object, Field f) {
		f.setAccessible(true);

		Object valueToSet = value;
		try {
			// check if field type is enum
			if (Enum.class.isAssignableFrom(f.getType())) {
				Enum v = Enum.valueOf((Class<Enum>) f.getType(),
						String.valueOf(value));
				{
					valueToSet = v;
				}
			}
			if (f.getType().getName().equals("java.util.List")) {
				// Method[] ms = object.getClass().getMethods();
				Method method = MethodUtils.findMethod(object.getClass(), "add"
						+ StringUtils.capitalize(f.getName()),
						new Class[] { value.getClass() });
				method.invoke(object, value);
			} else {
				f.set(object, valueToSet);
			}
		} catch (Exception e) {
			throw new RuntimeException(e.getMessage(), e);
		}
	}

	public Object get(String field) {
		if (target == null) {
			return null;
		}

		return get(field, field, target);
	}

	private Object doGetFieldValue(String fullField, String field,
			Object object, boolean useCache,
			Map<String, ReflectInfo> cachedFields) {
		// check cache
		Field f;
		if (useCache) {
			ReflectInfo info = cachedFields.get(fullField);
			if (info != null) {
				return getField(info.target, info.field);
			}
		}

		int index = field.indexOf('.');
		if (index != -1) {
			String parent = field.substring(0, index);
			String sub = field.substring(index + 1);

			try {
				f = FieldUtils.findField(object.getClass(), parent);
				if (f == null) {
					throw new RuntimeException("No field '" + parent
							+ "' found at class " + object.getClass().getName());
				}
				f.setAccessible(true);
				Object o = f.get(object);
				if (o == null) {
					return null;
				}
				return get(fullField, sub, o);
			} catch (Exception e) {
				throw new RuntimeException(e.getMessage(), e);
			}
		}

		f = FieldUtils.findField(object.getClass(), field);
		if (f == null) {
			throw new RuntimeException("No field '" + field
					+ "' found at class " + object.getClass().getName());
		}
		if (useCache && !cachedFields.containsKey(fullField)) {
			cachedFields.put(fullField, new ReflectInfo(f, object));
		}
		return getField(object, f);
	}

	/**
	 * @param field
	 * @param target2
	 * @return
	 */
	private Object get(String fullField, String field, Object object) {
		return doGetFieldValue(fullField, field, object, this.cached,
				this.cachedFields);

	}

	/**
	 * @param object
	 * @param f
	 * @return
	 * @throws IllegalAccessException
	 */
	private Object getField(Object object, Field f) {
		f.setAccessible(true);
		try {
			return f.get(object);
		} catch (Exception e) {
			throw new RuntimeException(e.getMessage(), e);
		}
	}

	public byte[] encode() throws IOException {
		return codec.encode(target);
	}

	public IDLProxyObject decode(byte[] bb) throws IOException {
		if (bb == null) {
			throw new IllegalArgumentException("param 'bb' is null");
		}
		Object object = codec.decode(bb);
		return new IDLProxyObject(codec, object, cls);

	}

	public void clearFieldCache() {
		cachedFields.clear();
	}

	/**
	 * get the target
	 * 
	 * @return the target
	 */
	public Object getTarget() {
		return target;
	}

	public void setTarget(Object target) {
		this.target = target;
	}

	private static class ReflectInfo {
		private Field field;
		private Object target;

		/**
		 * @param field
		 * @param target
		 */
		public ReflectInfo(Field field, Object target) {
			super();
			this.field = field;
			this.target = target;
		}

	}
}
